﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using Adventure.Library;
using Adventure.Server.Data;
using Adventure.Server.Manager;
using Neitzel;

namespace Adventure.Server.Library
{
    /// <summary>
    /// SavedObject is the basis of all objects inside the game.
    /// </summary>
    public class SavedObject : IDisposable, INotifyPropertyChanged, INotifyPropertyChanging
    {
        #region Fields

        /// <summary>
        /// Tracesource for all log messages.
        /// </summary>
        private static readonly TraceSource Logger = new TraceSource(ServerConstants.ServerTraceSourceNameLibrary);

        /// <summary>
        /// Attributes of the Instance.
        /// </summary>
        protected readonly ObjectAttributes Attributes;

        #endregion

        #region Attributes

        /// <summary>
        /// The key of the identifier.
        /// </summary>
        public const string IdentifierAttribute = "Id";

        /// <summary>
        /// The class name of the object.
        /// </summary>
        /// <remarks>
        /// This can be just the full class name or also include an Assembly name.
        /// </remarks>
        public const string ClassNameAttribute = "ClassName";

        /// <summary>
        /// The key for the name of the object;
        /// </summary>
        public const string NameAttribute = "Name";

        #endregion

        #region Lifetime

        /// <summary>
        /// Creates a new instance of SavedObject.
        /// </summary>
        public SavedObject()
        {
            // New object attributes instance
            Attributes = new ObjectAttributes();

            // Reference counter is 1 because we create a new instance.
            ReferenceCounter = 1; 
        }

        /// <summary>
        /// Dispose this instance
        /// </summary>
        /// <remarks>
        /// This decreases the referene counter only. So please make sure that you only dispose exactly once!
        /// </remarks>
        public void Dispose()
        {
            Dispose(true);
        }

        /// <summary>
        /// Dispose this instance
        /// </summary>
        /// <remarks>
        /// This decreases the referene counter only. So please make sure that you only dispose exactly once!
        /// </remarks>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                ReferenceCounter--;
            }
        }

        #endregion

        #region Operators

        /// <summary>
        /// Tests two objects of this type for equality by value.
        /// </summary>
        public static bool operator ==(SavedObject obj1, SavedObject obj2)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(obj1, obj2))
                return true;

            // If left hand side is null return false.
            if (ReferenceEquals(obj1, null))
                return false;

            // Return compare of both objects.
            return obj1.Equals(obj2);
        }

        /// <summary>
        /// Tests two objects of this type for inequality by value.
        /// </summary>
        public static bool operator !=(SavedObject obj1, SavedObject obj2)
        {
            return !(obj1 == obj2);
        }

        /// <summary>
        /// Compares this object with another by value.
        /// </summary>
        public override bool Equals(object obj)
        {
            // Compare nullability and type
            var other = obj as SavedObject;
            if (ReferenceEquals(other, null))
                return false;

            // Return true if the fields match:
            return Attributes.Equals(other.Attributes) &&
                other.IsChanged == IsChanged &&
                other.Id == Id;
        }

        /// <summary>
        /// Returns a hashcode based on the current value of this object.
        /// </summary>
        public override int GetHashCode()
        {
            return Attributes.GetHashCode() ^
                (Id??"").GetHashCode()>>2; // Moving by 2 bit else we exclude the id when it is stored inside the attributes.
        }

        #endregion

        #region Static Elements

        /// <summary>
        /// Creates a new SavedObject.
        /// </summary>
        /// <remarks>
        /// This method creates a new SavedObject but it is not registered inside the ObjectManager.
        /// Please use the ObjectManager to create a new in game object that is saved.
        /// </remarks>
        /// <param name="attributes">Attributes of the SavedObject to create.</param>
        /// <returns>The SavedObject or null</returns>
        public static SavedObject Create(ObjectAttributes attributes)
        {
            // validate arguments
            if (attributes==null) throw new ArgumentNullException(nameof(attributes));

            string className = "";
            try
            {
                // Get the class from the attributes
                className = attributes[ClassNameAttribute];

                // Create the instance
                var savedObject = TypeExtensions.CreateInstance<SavedObject>(className);
          
                // Set the class to make sure that the classname does not contain version, culture and public key.
                attributes[ClassNameAttribute] = savedObject.GetType().FullName + ", " +
                                                 savedObject.GetType().Assembly.GetName().Name;

                // Initialize the object.
                savedObject.Init(attributes);

                // return result.
                return savedObject;
            }
            catch (Exception ex)
            {
                // Log the exception and return null.
                Logger.TraceEvent(TraceEventType.Error, (int)ServerTraceId.Library, "Unable to create new object of type {0}. Exception: {1} / {2}", className, ex.Message, ex.StackTrace);
                return null;
            }
        }

        /// <summary>
        /// ObjectManager of this object.
        /// </summary>
        protected static IObjectManager ObjectManager { get; private set; }

        /// <summary>
        /// Set the object manager
        /// </summary>
        /// <param name="manager">ObjectManager instance to use.</param>
        public static void SetObjectManager(IObjectManager manager)
        {
            // Validation
            if (manager == null) throw new ArgumentNullException(nameof(manager));
            if (ObjectManager != null)
            {
                var message = Properties.Resources.ObjectManagerCanOnlyBeSetOnce;
                Logger.TraceEvent(TraceEventType.Error, (int)ServerTraceId.Library, message);
                throw new InvalidOperationException(message);
            }

            // Set the ObjectManager
            ObjectManager = manager;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Reference counter of this object.
        /// </summary>
        public int ReferenceCounter { get; protected set; }

        /// <summary>
        /// Increase the reference counter.
        /// </summary>
        public void IncreaseReferenceCounter()
        {
            ReferenceCounter++; 
        }

        /// <summary>
        /// Is this instance changed or not?
        /// </summary>
        public bool IsChanged { get; set; }

        /// <summary>
        /// Id of this object.
        /// </summary>
        public string Id
        {
            get { return _id; }
            private set
            {
                OnPropertyChanging(nameof(Id));
                _id = value;
                OnPropertyChanged(nameof(Id));
            }
        }
        private string _id;

        #endregion

        #region Events

        /// <summary>
        /// Property was changed Event.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Property will be changed.
        /// </summary>
        public event PropertyChangingEventHandler PropertyChanging;

        #endregion

        #region Public Methods

        /// <summary>
        /// Save this object.
        /// </summary>
        public void Save()
        {
            // Only save if object was changed.
            if (!IsChanged)
                return; 

            // Verify that we can safe the object.
            if (ObjectManager == null)
            {
                var message = string.Format(CultureInfo.InvariantCulture, Properties.Resources.ObjectManagerNotSetError, Id);
                Logger.TraceEvent(TraceEventType.Error, (int)ServerTraceId.Library, message);
                throw new AdventureException(message);
            }

            // Save the object.
            ObjectManager.Save(this);
        }

        /// <summary>
        /// Saves the object through the data adapter given.
        /// </summary>
        /// <param name="adapter">Adapter to save this object to.</param>
        public void Save(IDataAdapter adapter)
        {
            // validate
            if (adapter == null) throw new ArgumentNullException(nameof(adapter));

            Logger.TraceEvent(TraceEventType.Verbose, (int)ServerTraceId.Library, Properties.Resources.SavingObjectMessage, Id);
            PrepareSave();
            IsChanged = false;
            adapter.SaveObjectAttributes(Attributes);
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Prepare save of this object.
        /// </summary>
        protected virtual void PrepareSave()
        {
        }

        /// <summary>
        /// Initialize the SavedObject with the given attributes.
        /// </summary>
        /// <param name="attributes">Attributes of the new object.</param>
        protected virtual void Init(ObjectAttributes attributes)
        {
            // validate arguments
            if (attributes == null) throw new ArgumentNullException(nameof(attributes));

            // Put all attributes into the local attributes.
            foreach (var element in attributes)
                Attributes[element.Key] = element.Value;

            // Set the local properties
            _id = Attributes[IdentifierAttribute];
        }

        /// <summary>
        /// Property was changed
        /// </summary>
        /// <param name="propertyName">Name of property that was changed.</param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            var handle = PropertyChanged;
            handle?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Property will change.
        /// </summary>
        /// <param name="propertyName">Name of property that will be changed.</param>
        protected virtual void OnPropertyChanging(string propertyName)
        {
            var handle = PropertyChanging;
            handle?.Invoke(this, new PropertyChangingEventArgs(propertyName));
        }

        #endregion
    }
}
